//
// Copyright (c) 2007-2010 Intrepid Services, LLC.  All rights reserved.
// Use, duplication, or modification subject to license, consulting, and/or employment agreement(s).
//
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;


namespace ConstructionBidOnline.Biz
{
	/// <summary>
	/// Base class for the application's business objects.
	/// </summary>
	[Serializable]
	public abstract class BOBase
	{
		#region Types

		#region ObjectState Enums

		/// <summary>
		/// Represents the state of the data within the object and has
		/// the same meanings as the System.Data.DataRowState enumeration.
		/// </summary>
		public enum ObjectState
		{
			/// <summary>
			/// The object is new and presumed not to exist in the backend database.
			/// </summary>
			Added,
			
			/// <summary>
			/// The object is pending deletion from the backend database.
			/// </summary>
			Deleted,
			
			/// <summary>
			/// One or more properties of the object have been modified since last retrieved from the backend database.
			/// </summary>
			Modified,
			
			/// <summary>
			/// The object has not been modified since last being retrieved from the backend database.
			/// </summary>
			Unchanged,
			
			/// <summary>
			/// The object's properties are in their default, just-created state. The object is not considered valid in this state and will cause an exception if passed to business object methods that attempt to modify the backend database.
			/// </summary>
			Uninitialized
		}

		#endregion

		#region ValidationError

		/// <summary>
		/// Used to encapsulate an error returned from the Validate() method.
		/// </summary>
		[Serializable]
        public class ValidationError
		{
			#region Types

			public enum Severity
			{
				Info,
				Warning,
				Error
			}

			#endregion

			#region Construction / Disposal

			public ValidationError() { }

			public ValidationError(string element, string msg, Severity severity)
			{
				Element = element;
				Message = msg;
				ErrorSeverity = severity;
			}

			#endregion

			#region Public Properties

			/// <summary>
			/// The source of the error; typically the name of a Property that failed validation.
			/// </summary>
			public string Element
			{
				get { return (_element); }
				set { _element = value; }
			} private string _element = "";


			/// <summary>
			/// The text of the error being described.
			/// </summary>
			public string Message
			{
				get { return (_message); }
				set { _message = value; }
			} private string _message = "";


			/// <summary>
			/// The severity-level of the error being described.
			/// </summary>
			public Severity ErrorSeverity
			{
				get { return (_severity); }
				set { _severity = value; }
			} private Severity _severity = Severity.Info;

			#endregion
		}

		#endregion

		#region Event Argument Classes & Delegates

		/// <summary>
		/// Encapsulates the Property Changed event data.
		/// </summary>
		public class PropertyChangedEventArgs : System.EventArgs
		{
			#region Construction / Disposal

			public PropertyChangedEventArgs() { }

			public PropertyChangedEventArgs(string propertyName, object oldValue, object newValue)
			{
				PropertyName = propertyName;
				OldValue = oldValue;
				NewValue = newValue;
			}

			#endregion

			#region Public Properties

			public string PropertyName
			{
				get { return (_propertyName); }
				set { _propertyName = value; }
			} private string _propertyName = "";


			public object OldValue
			{
				get { return (_oldValue); }
				set { _oldValue = value; }
			}  private object _oldValue = null;


			public object NewValue
			{
				get { return (_newValue); }
				set { _newValue = value; }
			}  private object _newValue = null;

			#endregion
		}


		public class BeforePropertyChangedEventArgs : PropertyChangedEventArgs
		{
			#region Construction / Disposal

			public BeforePropertyChangedEventArgs() { }

			public BeforePropertyChangedEventArgs(string propertyName, object oldValue, object newValue)
				: base(propertyName, oldValue, newValue)
			{
			}

			#endregion

			#region Public Properties

			/// <summary>
			/// Set to true to cancel the property change.
			/// </summary>
			public bool CancelChange
			{
				get { return (_cancelChange); }
				set { _cancelChange = value; }
			} private bool _cancelChange = false;

			#endregion
		}


		public class AfterPropertyChangedEventArgs : PropertyChangedEventArgs
		{
			#region Construction / Disposal

			public AfterPropertyChangedEventArgs() { }

			public AfterPropertyChangedEventArgs(string propertyName, object oldValue, object newValue)
				: base(propertyName, oldValue, newValue)
			{
			}

			#endregion
		}


		public delegate void BeforePropertyChangedEventHandler(object sender, BeforePropertyChangedEventArgs evt);

		public delegate void AfterPropertyChangedEventHandler(object sender, AfterPropertyChangedEventArgs evt);

		#endregion

		#region InvalidObjectException

		/// <summary>
		/// Exception thrown when an operation (e.g. Update()) is attempted on an object whose Validate() method returns false.
		/// </summary>
		public class InvalidObjectException : ApplicationException
		{
			public InvalidObjectException(string description) : base(description) { }
		}

		#endregion

		#endregion

		#region Public Properties

		/// <summary>
		/// Gets/sets the object's state (e.g. Added, Deleted, Modified, Unchanged, Uninitialized.)
		/// </summary>
		public ObjectState ObjState
		{
			get { return (_objState); }
			set { _objState = value; }
		}  private ObjectState _objState = ObjectState.Uninitialized;


		/// <summary>
		/// Validation errors list.
		/// </summary>
		public List<ValidationError> Errors
		{
			get { return (_errList); }
		} private List<ValidationError> _errList = new List<ValidationError>();


		/// <summary>
		/// True iff one or more of the Validation Errors has a Severity equal to Error.
		/// </summary>
		public bool HasErrors
		{
			get
			{
				if (Errors.Count == 0)
					return (false);

				bool hasErr = false;
				foreach (ValidationError err in Errors)
				{
					if (err.ErrorSeverity == ValidationError.Severity.Error)
					{
						hasErr = true;
						break;
					}
				}
				return( hasErr);
			}
		}

		#endregion

		#region Protected Properties

		/// <summary>
		/// True iff the object is being initialized.
		/// </summary>
		protected bool IsInitializing
		{
			get { return (_isInitializing); }
			set { _isInitializing = value; }
		}  private bool _isInitializing = false;

		#endregion

		#region Public Events

		/// <summary>
		/// Event fired before a property is changed.
		/// </summary>
		public event BeforePropertyChangedEventHandler BeforePropertyChanged;

		/// <summary>
		/// Event fired after a property is changed.
		/// </summary>
		public event AfterPropertyChangedEventHandler AfterPropertyChanged;

		#endregion

		#region Abstract Methods

		public abstract bool Validate();

		#endregion

		#region Protected Methods

		#region Error List Helpers

		/// <summary>
		/// Adds the specified ValidationError to the list exposed by the Errors property.
		/// </summary>
		protected void AddError(ValidationError err)
		{
			// TODO: OnError event ???

			this._errList.Add(err);
		}


		/// <summary>
		/// Removes all ValidationErrors from the list exposed by the Errors property.
		/// </summary>
		protected void ClearErrors()
		{
			this._errList.Clear();
		}

		#endregion

		#region OnPropertyChanging

		protected bool OnPropertyChanging( string propertyName, object currentValue, object proposedNewValue)
		{
			// Suppress events iff the object is in the middle of an O-R mapping operation.
			if ( IsInitializing )
				return (true);

			// If nobody's listening for the prop change, then it must be ok to change it.
			if (BeforePropertyChanged == null)
				return (true);

			// Fire the BeforePropertyChanged event.
			BeforePropertyChangedEventArgs evt = new BeforePropertyChangedEventArgs( propertyName, currentValue, proposedNewValue);
			BeforePropertyChanged( this, evt);

			return( ! evt.CancelChange );
		}

		#endregion

		#region OnPropertyChanged

        protected void OnPropertyChanged(string propertyName, object oldValue, object newValue)
		{
            // Suppress events iff the object is in the middle of an O-R mapping operation.
			if ( IsInitializing )
				return;
			
			// If our state is Unchanged (or unknown), then make it Modified.
			if ((ObjState == ObjectState.Unchanged) || (ObjState == ObjectState.Uninitialized))
				ObjState = ObjectState.Modified;

			// Fire the AfterPropertyChanged event.
			if (AfterPropertyChanged != null)
				AfterPropertyChanged( this, new AfterPropertyChangedEventArgs( propertyName, oldValue, newValue));
		}

		#endregion

		#region Object-Relational Mapping (ORM) Helpers

		#region RelationalToObject

		/// <summary>
		/// Transfers values from the columns of the specified row into the
		/// identically named public properties of the specified object.
		/// Note that read-only object properties and object properties
		/// implementing IList (i.e. child collections) are ignored.
		/// </summary>
		protected static void RelationalToObject(BOBase obj, DataRow row)
		{
			obj.IsInitializing = true;

			// Get Object Properties
			PropertyDescriptorCollection props = TypeDescriptor.GetProperties(obj);

			// Apply OR-Mapping
			foreach (DataColumn column in row.Table.Columns)
			{
				for (int ndx = 0; ndx < props.Count; ndx++)
				{
					PropertyDescriptor prop;
					try
					{
						prop = props[ ndx ];
					}
					catch
					{
						continue;
					}

					// Omit IListTypes (childcollections)
					bool isLookup = false;
					string propName, colName;
					if ((prop.PropertyType.GetInterface("IList") == null) && (!prop.IsReadOnly))
					{
						colName = column.ColumnName.Trim().ToLower();
						isLookup = colName.StartsWith("lu_");
                        propName = isLookup ? prop.Name.ToLower().StartsWith("lu_") ? prop.Name.Trim().ToLower() : "lu_" + prop.Name.Trim().ToLower() : prop.Name.Trim().ToLower();

						if (propName == colName)
						{
							if (!row.IsNull(column))
							{
								prop.SetValue(obj, row[ column ]);
							}
							else
							{
								if (!prop.PropertyType.IsValueType)
								{
									prop.SetValue(obj, null);
								}
								else if (prop.PropertyType.FullName.ToLower().StartsWith("system.nullable"))
								{
									prop.SetValue(obj, null);
								}
								else
								{
									// Primitive types get whatever is the default value assigned by the object or the
									// default value for the type if the object does not explicitly assign a default value.
								}
							}
						}
					}
				}
			}

			// Convey the DataRowState to the object's ObjState property.
			SetObjectStateFromRowState(obj, row);

			obj.IsInitializing = false;
		}

		#endregion

		#region ObjectToRelational

		/// <summary>
		/// Transfers values from the public properties of the specified
		/// object into the identically named columns of the specified row.
		/// Note that read-only object properties and object properties
		/// implementing IList (i.e. child collections) are ignored.
		/// </summary>
		protected static void ObjectToRelational(BOBase obj, DataTable tbl, DataRow row)
		{
			obj.IsInitializing = true;

			// Get Object Properties
			PropertyDescriptorCollection props = TypeDescriptor.GetProperties(obj);

			// Loop trough the object properties
			for (int ndx = 0; ndx < props.Count; ndx++)
			{
				// Get the property descriptor.
				PropertyDescriptor prop = props[ndx];

				// Ignore non-Public properties.
				if (prop.PropertyType.IsNotPublic) continue;

				// Ignore Properties that are child collections.
				if (prop.PropertyType.GetInterface("IList") != null) continue;

				// Map data to appropriate row column.
				string colName = row.Table.Columns.Contains(prop.Name) ? prop.Name : (row.Table.Columns.Contains("LU_" + prop.Name) ? "LU_" + prop.Name : "");

				// If there's no column matching either 'prop.Name' nor 'LU_prop.Name', then keep loopin'...
				if (colName == "") continue;

				try
				{
					object val = prop.GetValue(obj);

					if ((val is DateTime) && (((DateTime)val == DateTime.MinValue) || ((DateTime)val == DateTime.MaxValue)))
					{
						// Convert DateTime.Min/max -> DBNull.Value.
						row[ colName ] = DBNull.Value;
					}
					else if (val != null)
					{
						row[ colName ] = val;
					}
					else
					{
						// Convert NULL -> DBNull.Value.
						row[ colName ] = DBNull.Value;
					}
				}
				catch (NotImplementedException ex)
				{
					throw ex;
				}
				catch (Exception ex)
				{
					string msg = ex.Message;
				}
			}

			if (row.RowState == DataRowState.Detached)
				tbl.Rows.Add(row);

			// Convey the object's ObjState to the row's DataRowState.
			SetRowStateFromObjectState(obj, row);

			obj.IsInitializing = false;
		}

		#endregion

		#region SetObjectStateFromRowState

		protected static void SetObjectStateFromRowState( BOBase obj, DataRow row)
		{
			switch( row.RowState )
			{
				case DataRowState.Added:
					obj.ObjState = ObjectState.Added;
					break;

				case DataRowState.Deleted:
					obj.ObjState = ObjectState.Deleted;
					break;

				case DataRowState.Modified:
					obj.ObjState = ObjectState.Modified;
					break;

				case DataRowState.Unchanged:
					obj.ObjState = ObjectState.Unchanged;
					break;

				case DataRowState.Detached:
					obj.ObjState = ObjectState.Uninitialized;
					break;
			}
		}

		#endregion

		#region SetRowStateFromObjectState

		protected static void SetRowStateFromObjectState( BOBase obj, DataRow row)
		{
			switch( obj.ObjState )
			{
				case ObjectState.Added:
					if (row.RowState != DataRowState.Added)
					{
						row.AcceptChanges();
						row.SetAdded();
					}
					break;

				case ObjectState.Deleted:
					if (row.RowState != DataRowState.Deleted)
					{
						row.AcceptChanges();
						row.Delete();
					}
					break;

				case ObjectState.Modified:
					if (row.RowState != DataRowState.Modified)
					{
						row.AcceptChanges();
						row.SetModified();
					}
					break;

				case ObjectState.Unchanged:
					row.RejectChanges();
					break;

				case ObjectState.Uninitialized:
					throw new NotSupportedException("The requested operation 'set DataRowState to uninitialized' could not be performed; there is no mapping between these states.");
			}
		}

		#endregion

		#endregion

		#endregion
	}
}
